문단 클릭 = 시작 위치 선택 · 재생은 우하단 버튼/P (재생 중 클릭 = 점프)

EP0. 4주차 — 실무 RAG: pgvector 멀티벡터 스토어와 하이브리드 검색

이 노트는 "책에 있는 예제를 그대로 따라 하면 장난감이라서 실무에 전혀 쓸 수가 없다"는 문제의식에서 출발한다. 강의는 인메모리(FAISS·BM25Retriever) 예제를 실무 규모(10만~20만 건)에서 돌아가는 pgvector 기반 검색 엔진으로 다시 짓는 과정을 전 구간에 걸쳐 실습한다. 아래 내용은 강사의 실제 발화(자막)를 1차 근거로, 슬라이드 설명과 화면 OCR을 보조 근거로 정리했다.


0. 사전 필수 용어 (선행지식)

본문을 읽기 전에 다음 용어의 감을 잡아 두면 훨씬 수월하다. 입문자·고등학생 눈높이로 정리한다.

  1. 임베딩(Embedding) / 벡터(Vector) 글(문장·단어)을 숫자 배열로 바꾼 것이다. 비슷한 뜻의 글은 숫자 배열도 서로 가깝게 놓이도록 만든다. "왕 − 남자 + 여자 ≈ 여왕" 같은 계산이 되는 좌표라고 생각하면 된다. 이 강의에서는 4000차원(숫자 4000개)짜리 임베딩을 쓴다. → §3·§6에서 계속 등장한다.

  2. 청크(Chunk) / 청킹(Chunking) 긴 문서를 검색·저장하기 좋게 잘게 쪼갠 조각이다. 책 한 권을 통째로 벡터로 만들면 "어느 문단이 답인지" 알 수 없으므로, 문단·문장 단위로 잘라서 각 조각을 벡터로 만든다. 조각이 너무 크면 잡음이 섞이고, 너무 작으면 문맥이 끊긴다. → §4 Step 1, §7 Parent-Child에서 핵심이 된다.

  3. 벡터 스토어(Vector Store) 임베딩(벡터)들을 저장하고, "이 질문 벡터와 가장 가까운 벡터 N개"를 빠르게 찾아 주는 데이터베이스다. 이 강의는 실습 초반엔 크로마DB(ChromaDB), 실무 파트에선 pgvector(PostgreSQL 확장)를 쓴다. → §2·§4·§6.

  4. 리트리버(Retriever) "질문을 받으면 관련 문서 조각을 꺼내 오는 부품"이다. LangChain에서 검색기의 공통 인터페이스이며, 무엇을 어떻게 꺼내느냐에 따라 ParentDocumentRetriever·MultiQueryRetriever·EnsembleRetriever 등으로 나뉜다. → §4·§7 전체.

  5. RAG(Retrieval-Augmented Generation, 검색 증강 생성) LLM에게 답을 시키기 전에 먼저 관련 문서를 검색(Retrieval)해서 프롬프트에 넣어 주고, 그 근거를 바탕으로 답을 생성(Generation)하게 하는 구조다. LLM의 환각을 줄이고 최신·사내 데이터를 반영하는 표준 기법이다. → 이 노트 전체의 큰 그림.

📚 참고: 선행지식이 부족하다면 임베딩·벡터 스토어의 기초 개념부터 잡는 것이 좋다. LangChain 한국어 튜토리얼(강의 화면에도 등장)과 각 벡터 DB 공식 문서(§5)가 좋은 출발점이다.


1. 주제 정의

이 강의의 주제는 "교과서 수준의 RAG 검색을 실무에서 버티는 검색 엔진으로 끌어올리는 것" 이다. 강사는 첫 문장부터 못을 박는다. 책에 나오는 파시스(FAISS)나 BM25Retriever 같은 것은 전부 인메모리(in-memory)라서, 데이터가 10만·20만 건씩 들어오는 실무에서는 처리할 수 없다는 것이다. 그래서 이 강의는 두 축으로 움직인다.

  • 축 1 — 멀티벡터 스토어(Multi-Vector Store) 패턴: 검색 품질을 올리기 위해 "벡터를 여러 개 더 잡는" 기법들의 묶음. 오늘 배우는 대부분(Parent-Child Splitting, Multi-Query Retriever, HYDE)이 이 큰 그림 안에 들어간다. 확장의 방향은 두 가지다 — 질의 확장(질문을 여러 개로)과 답변 확장(문서를 여러 개로).
  • 축 2 — 저장소를 pgvector로 이전: 인메모리 스토어를 걷어내고 PostgreSQL(pgvector) 위에서 BM25(희소 벡터)와 Dense(밀집 벡터) 검색을 모두 구현한 뒤, 둘을 앙상블(RRF)로 통합한다.

강사의 정의를 그대로 옮기면, 오늘 다루는 기법은 "대부분 다 멀티벡터 스토어 패턴 중 하나"이며, 그 위에 "저장소·인덱스·앙상블"이라는 실무 인프라를 얹는 것이 이 강의의 본질이다.

💡 실무 노하우: 강사는 파이썬/LangChain으로 구축하는 것이 무의미하지 않다고 강조한다. "한 번 파이썬으로 어떻게 구축되는지만 이해하면, 이 코드를 바탕으로 Rust나 TypeScript로 옮기는 건 전부 AI에게 시키면 된다." 즉 파이썬 구현은 개념을 확정하는 청사진이고, 고성능이 필요하면 그 청사진을 고성능 언어로 번역하거나 특정 기능만 API로 빼서 도커에 올린다. (00:04:04, 00:07:33)


2. 풀려는 문제 — 검색 시나리오

RAG가 존재하는 이유는 네 가지 문제를 동시에 풀기 위해서다.

  • 환각 회피: LLM이 모르는 것을 지어내지 않도록, 실제 근거 문서를 먼저 찾아 프롬프트에 넣는다.
  • 정확도: 사용자의 질문 의도에 맞는 조각을 정확히 골라야 최종 답이 좋아진다.
  • 최신성: 모델을 재학습하지 않고도 최신 문서를 검색으로 반영한다.
  • private 데이터: 사내 문서·개인 데이터처럼 학습에 넣을 수 없는 자료를 검색으로 붙인다.

이 강의가 특히 파고드는 문제는 규모(scale) 다. 강사의 표현대로 책의 예제는 "장난감 수준"이고, 데이터가 10만·20만 건 들어오면 인메모리로는 서버 안정성 측면에서도 감당이 안 된다. 그래서 문제는 이렇게 좁혀진다.

"대용량 문서를 안정적으로 저장하면서, 키워드 검색(BM25)과 의미 검색(Dense)을 둘 다, 실무 인프라(PostgreSQL) 위에서 어떻게 구현하고 통합할 것인가?"

여기에 한글 특유의 문제가 하나 더 붙는다. PostgreSQL은 기본적으로 한글 형태소 분석기가 없어서 한글 텍스트를 제대로 토큰화하지 못한다. 그래서 MeCab-ko 형태소 분석기를 pgvector 컨테이너에 직접 심어야 BM25가 한글에서 동작한다. (자막 00:41:23 "한글 형태소 분석기가 기본적으로 이 안에 없어")

⚠️ 주의: "검색이 잘 되겠지"라는 낙관은 금물이다. 강사는 뒤에서 PostgreSQL 내장 ts_rank_cd로 긴 문장을 질의하면 AND 조건 때문에 결과가 0건으로 나오는 경우가 흔하다고 경고한다. 검색 정확도는 저절로 얻어지는 것이 아니라, 랭킹 함수와 형태소 분석과 인덱스를 손으로 맞춰야 얻어진다.


3. 핵심 구조 (RAG 파이프라인)

전체 파이프라인은 "문서를 넣는 쪽(인덱싱)"과 "질문을 처리하는 쪽(검색·생성)"으로 나뉜다.

flowchart TD
    A[원본 문서 PDF/TXT] --> B[TextLoader 로 로딩]
    B --> C[청크 분할<br/>RecursiveCharacterTextSplitter<br/>chunk_size=1000 / 200]
    C --> D1[임베딩 생성<br/>4000차원 모델]
    C --> D2[형태소 분석<br/>MeCab-ko → ts_vector]
    D1 --> E1[Dense 저장<br/>pgvector + HNSW]
    D2 --> E2[BM25 통계 저장<br/>tf / df / 문서길이 + Gin]
    Q[사용자 질문] --> R1[Dense Retriever<br/>PGVector, cosine]
    Q --> R2[BM25 Retriever<br/>BaseRetriever 랩핑]
    E1 --> R1
    E2 --> R2
    R1 --> F[EnsembleRetriever<br/>RRF 통합]
    R2 --> F
    F --> G[RetrievalQA 체인]
    G --> H[LLM 최종 답변]

핵심은 한 벌의 문서가 두 갈래로 색인된다는 점이다. 같은 청크가 한쪽에서는 밀집 벡터(임베딩)로, 다른 쪽에서는 희소 벡터(형태소 기반 ts_vector)로 저장된다. 질문이 들어오면 두 리트리버가 각자 후보를 뽑고, EnsembleRetriever가 RRF로 순위를 섞어 최종 근거를 만들고, 그것을 RetrievalQA가 LLM에게 넘긴다.

멀티벡터 스토어 관점에서 보면, 이 구조 안에는 질의 확장(Multi-Query, HYDE)과 답변 확장(Parent-Child)이라는 두 종류의 "여러 개로 잡기"가 선택적으로 끼어들 수 있다. §7에서 각각을 다룬다.


4. 구현 가이드 (Do It Yourself)

시작 전

  • Python 3.xLangChain 설치 — RAG 구성요소를 부품처럼 조립하기 위한 프레임워크.
  • 임베딩 모델 — 강의는 로컬 LM Studio로 서빙하는 4000차원 임베딩 모델을 쓴다(화면상 Qwen3-Embedding-8B GGUF). API 임베딩(예: text-embedding-3-small)으로 대체 가능.
  • Docker — pgvector와 pgAdmin을 로컬에 띄우기 위한 컨테이너 플랫폼.
  • pgvector(PostgreSQL + vector 확장) — 실무 스케일 벡터 저장소. 이미지 pgvector/pgvector:pg17 기반에 MeCab-ko를 추가로 빌드한다.

⚠️ 주의: 로컬 임베딩 서버(LM Studio)는 한 번에 받는 텍스트 양이 많으면 연결이 끊긴다. 화면 주석에도 "chunk_size를 작게" 두라는 메모가 보인다(check_embedding_ctx_length=False). 초기 학습 시 배치 크기를 무리하게 올리지 말 것.

Step 1 — 문서 로딩·청크 분할

문서를 로컬 경로(예: practice4)에서 읽어 청크로 자른다. 부모-자식 분할을 쓸 것이므로 큰 단위(1000)와 작은 단위(200) 두 기준을 잡는다.

from langchain.text_splitter import RecursiveCharacterTextSplitter

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=50)

💡 실무 노하우: chunk_size는 임베딩 모델의 context limit과 "의미 단위"의 균형에서 정한다. 일반적으로 256~1024 토큰 사이가 무난하다. 강사는 책의 고정 분할보다 시맨틱 분할(의미 기준 분할) 이 더 낫다고 언급하며, 부모-자식이 "유의미한 관계"로 나뉘게 하는 것이 목표라고 강조한다. (00:09:59, 00:08:54)

Step 2 — 임베딩 생성

각 청크를 벡터로 만든다. 강의는 4000차원 모델을 쓰되, 로컬 서버 안정성을 위해 배치 처리 옵션을 조정한다.

from langchain_openai import OpenAIEmbeddings  # LM Studio 호환 엔드포인트

embeddings = OpenAIEmbeddings(
    model="Qwen3-Embedding-8B",
    base_url="http://localhost:1234/v1",   # LM Studio 로컬 서버
    check_embedding_ctx_length=False,
)

📚 참고: 4000차원은 검색 정확도엔 유리하지만, 뒤에서 HNSW 인덱스를 만들 때 차원이 너무 높으면 문제가 생긴다. 그래서 §7의 MRL 차원 축소가 필요해진다.

Step 3 — 벡터 스토어 저장 (크로마DB로 개념 확인 → pgvector로 실무 이전)

먼저 크로마DB로 개념을 확인한 뒤, 실무에서는 pgvector로 옮긴다. 부모-자식 분할은 두 저장소를 쓴다 — 자식 청크는 벡터 스토어에, 부모 원문은 문서 스토어(Document Store)에 넣고 메타데이터로 ID를 연결한다.

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore  # 실무에선 pgvector/DB 스토어로 교체
from langchain_chroma import Chroma

vectorstore = Chroma(collection_name="child_chunks", embedding_function=embeddings)
docstore = InMemoryStore()  # 강사 경고: 부모를 인메모리에 두는 건 실무 불가

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=docstore,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)
retriever.add_documents(docs)

⚠️ 주의: 노트북을 여러 번 실행하면 컬렉션에 같은 데이터가 중복 적재되어 검색이 오염된다. 강사는 초기화(제거 후 재설정) 로직을 넣은 ChromaParentStore를 별도로 구성했다. 재실행 안전성은 실습 RAG의 흔한 함정이다. (00:09:52)

Step 4 — 검색·LLM 답변

리트리버로 후보를 뽑아 RetrievalQA 체인에 넘겨 LLM이 최종 답을 만든다.

from langchain.chains import RetrievalQA

qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
answer = qa.invoke("질문 내용")

전체 부모-자식 리트리버와 개별 벡터 스토어에 각각 같은 질문을 던져, 결과가 다르게(부모 원문 컨텍스트가 딸려 오는지) 나오는지 확인하는 것이 이 단계의 검증 포인트다.

확인: ① 임베딩이 정상 생성되는가 ② 문서가 중복 없이 로드되었는가 ③ 자식 청크로 검색했는데 부모 원문이 컨텍스트로 반환되는가.

Step 5 — pgvector·pgAdmin 환경 구축 (Docker)

여기서부터가 "장난감을 실무로" 바꾸는 구간이다. 인메모리를 걷어내고 PostgreSQL(pgvector)로 옮긴다. Docker Compose로 두 컨테이너를 띄운다 — 벡터 확장이 포함된 PostgreSQL과, 웹 관리 도구 pgAdmin이다.

# docker-compose.yml (강의 화면 재구성)
services:
  pgvector:
    build: .                       # FROM pgvector/pgvector:pg17 + MeCab-ko 빌드
    container_name: pgvector
    environment:
      POSTGRES_PASSWORD: postgres
    ports: ["5432:5432"]
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@docker

pgvector/pgvector:pg17 이미지 위에 한글 형태소 분석기 MeCab-ko 와 사전(dictionary)을 추가로 빌드해야 한다. 강사는 컨테이너에 형태소 분석기를 깔기 전에는 "한글 형태소 분석이 안 돼요"라고 못 박는다(00:41:23). pgAdmin은 PostgreSQL판 phpMyAdmin이라고 보면 된다 — 테이블·데이터·인덱스를 웹에서 시각적으로 확인한다.

⚠️ 주의: POSTGRES_PASSWORD 같은 자격 증명을 compose 파일에 평문으로 두는 것은 학습용이다. 실무에선 환경변수·시크릿 매니저로 분리한다.

Step 6 — PostgreSQL에서 BM25(희소 벡터) 직접 구현

BM25는 라이브러리가 대신 계산해 주지 않는다. 강사는 BM25 공식에 필요한 통계를 테이블 4종 + 트리거로 직접 관리한다.

  1. ts_vector 필드: MeCab-ko로 한글을 형태소 토큰으로 쪼갠 뒤, 토큰별 등장 횟수를 벡터화해 저장한다.
  2. tf 테이블: 문서 ID별 토큰 등장 횟수(Term Frequency).
  3. df 테이블: 각 토큰이 등장한 문서의 수(Document Frequency).
  4. 문서 길이 테이블: 전체 문서 수·총 토큰 길이를 저장해 평균 문서 길이를 동적으로 계산.
  5. Gin 인덱스: ts_vector에 걸어 토큰 검색을 가속.
  6. 트리거: 문서가 INSERT/UPDATE/DELETE 될 때마다 tf·df·평균 길이 테이블을 자동 갱신. "인설트를 할 때마다 트리거를 두 군데 건다"(00:45:22, 00:47:01).
-- Gin 인덱스 (ts_vector 검색 가속)
CREATE INDEX idx_docs_tsv ON documents USING gin (ts_vector);

-- BM25 점수를 CTE로 직접 계산 (개념 골격)
WITH stats AS (
  SELECT avg(doc_len) AS avgdl, count(*) AS n FROM doc_length
)
SELECT d.id,
       sum( idf.value * (tf.cnt * (k1 + 1))
            / (tf.cnt + k1 * (1 - b + b * d.doc_len / stats.avgdl)) ) AS score
FROM query_tokens q
JOIN tf  ON tf.token = q.token
JOIN idf ON idf.token = q.token
JOIN documents d ON d.id = tf.doc_id, stats
GROUP BY d.id
ORDER BY score DESC;

계산이 끝나면 이 SQL 검색을 LangChain BaseRetriever로 랩핑해, 프레임워크 안에서 다른 리트리버들과 똑같이 쓸 수 있게 만든다.

from langchain_core.retrievers import BaseRetriever

class PgBM25Retriever(BaseRetriever):
    def _get_relevant_documents(self, query, *, run_manager):
        rows = run_bm25_sql(query)      # 위 SQL 실행
        return [Document(page_content=r.text, metadata={"score": r.score}) for r in rows]

⚠️ 주의 — 먼저 내장 함수의 한계를 체감하라: 강사는 BM25를 직접 짜기 전에 PostgreSQL 내장 ts_rank_cd부터 써 본다. 긴 문장을 질의하면 AND 조건이 걸려 0건이 흔하다. 이 실패를 겪어야 왜 BM25의 시그마(합산)가 필요한지 몸으로 안다(00:54:51, 00:56:03).

💡 실무 노하우: MySQL이 아니라 PostgreSQL을 쓰는 이유가 바로 여기다. MySQL은 풀 텍스트 인덱싱 결과를 ts_vector처럼 외부 필드로 노출하지 않아 tf·df를 꺼내 BM25를 직접 만들 수 없다(00:58:53).

Step 7 — pgvector에서 Dense(밀집 벡터) 검색 + HNSW

같은 문서를 이번엔 임베딩(밀집 벡터)으로 색인한다. 4000차원은 HNSW 인덱스에 부담이 크므로 차원 축소를 먼저 한다.

  1. 차원 축소: MRL 학습된 모델이면 벡터 앞부분만 잘라 4000 → 1000차원으로 줄인다. MRL이 아니면 PCA를 쓴다.
  2. HNSW 인덱스 생성: 반드시 코사인 거리(cosine) 전략으로 만든다. 기본값은 내적(inner product)이므로 명시하지 않으면 엉뚱한 거리로 색인된다. M(연결 친구 수)·ef_construction(구축 시 탐색 범위)을 설정한다.
  3. LangChain PGVector 연동: 저장된 Dense 벡터를 별도 쿼리 구현 없이 리트리버로 감싼다.
-- HNSW 인덱스는 코사인 거리로 (L2/내적 아님)
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);
from langchain_postgres import PGVector

dense_store = PGVector(
    connection="postgresql+psycopg://postgres:postgres@localhost:5432/rag",
    embeddings=embeddings,
    collection_name="dense_docs",
)
dense_retriever = dense_store.as_retriever(search_kwargs={"k": 4})

강사: "hnsw 만들 때 코사인 디스턴스로 하겠다, L2·내적이 되지 않도록 코사인 디스턴스를 해줘야 돼요"(01:23:59). 그리고 검색 점수에서 음수 코사인은 버린다(01:40:40).

📚 참고: HNSW는 ANN(근사 최근접 이웃) 인덱스 중 "그럭저럭 괜찮은" 축에 든다(강사, 01:19:39). ChromaDB는 HNSW·IVF·IVF-PQ를 모두 지원하지만, pgvector는 HNSW·IVFFlat 중심이다. 인덱스 종류에 따라 색인 속도·정확도·메모리가 달라진다.

Step 8 — 앙상블(RRF)로 통합 + 최종 답변

BM25 리트리버와 Dense 리트리버를 EnsembleRetriever에 리스트로 넣고 가중치를 준다. 두 리트리버의 결과는 RRF(등수 기반)로 합쳐진다. 마지막에 RetrievalQA로 LLM 답변을 만든다.

from langchain.retrievers import EnsembleRetriever
from langchain.chains import RetrievalQA

ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, dense_retriever],
    weights=[0.5, 0.5],
)
qa = RetrievalQA.from_chain_type(llm=llm, retriever=ensemble)
print(qa.invoke("실무 RAG에서 인메모리 스토어를 왜 피해야 하나?"))

확인: ① BM25 검색이 0건이 아닌 유의미한 스코어를 내는가 ② Dense 검색에서 코사인 거리가 0에 가까운(유사한) 문서가 나오는가 ③ 앙상블이 두 신호를 섞어 단독보다 나은 근거를 만드는가 ④ 최종 LLM 답변이 검색 근거에 기반하는가.


5. 적용 사례 (RAG·Vector DB OSS)

강의는 특정 라이브러리에 종속되지 않고 개념을 옮길 수 있도록 설계되었다. 실무에서 조합할 수 있는 대표 OSS는 다음과 같다.

  • LangChain (https://github.com/langchain-ai/langchain) — 이 강의의 뼈대. ParentDocumentRetriever·MultiQueryRetriever·EnsembleRetriever·PGVector·RetrievalQA 제공.
  • LlamaIndex (https://github.com/run-llama/llama_index) — 인덱싱·쿼리 엔진 중심의 대안 프레임워크.
  • Chroma (https://github.com/chroma-core/chroma) — 개념 확인용 임베디드 벡터 DB. HNSW·IVF·IVF-PQ 지원.
  • Weaviate (https://github.com/weaviate/weaviate) — 하이브리드 검색을 기본 제공하는 벡터 DB.
  • Qdrant (https://github.com/qdrant/qdrant) — 고성능 벡터 검색 엔진(Rust 구현).
  • FAISS (https://github.com/facebookresearch/faiss) — 강의가 "장난감"이라 부른 인메모리 라이브러리. 개념 학습·소규모엔 여전히 유효.
  • sentence-transformers (https://github.com/UKPLab/sentence-transformers) — 임베딩 모델 학습·서빙 라이브러리.

강의가 실무 저장소로 선택한 것은 이 목록의 벡터 DB가 아니라 pgvector(PostgreSQL 확장)라는 점이 핵심이다. 이유는 §8에서 설명한다.

📚 참고: 화면에는 LangChain 한국어 튜토리얼(테디노트 계열)과 Hugging Face의 Qwen/Qwen3-Embedding-8B-GGUF 모델 카드가 등장한다. 실습을 재현하려면 이 두 자료가 직접적인 참고점이 된다.


6. 핵심 원리 — Hybrid Retrieval (BM25 + Dense Vector)

이 강의에서 가장 원리적인 대목이다. 하이브리드 검색은 희소 벡터(Sparse)밀집 벡터(Dense) 를 함께 쓴다. 둘의 본질이 다르다는 것을 이해해야 왜 둘을 섞는지가 보인다.

희소 벡터(Sparse Vector) — 공학으로 만든 벡터

희소 벡터는 각 차원이 완전히 분리된 독립적인 의미를 갖는다. 예컨대 어휘 사전에 토큰이 1800개면 벡터는 1800차원이고, 각 차원은 특정 토큰의 등장 정보를 뜻한다. 사람이 "공학적 사고방식"으로 만들 수 있다. 대표가 TF-IDFBM25다.

강사가 짚는 최적화 포인트: 실제로는 모든 레코드에 1800(또는 7889)차원을 다 채우지 않는다. 대부분의 값이 0이기 때문에(그래서 "희소"), 등장한 토큰만 저장한다. PostgreSQL의 ts_vector가 바로 이 자료구조다.

BM25는 키워드 랭킹 함수다. 두 축으로 점수를 낸다.

  • TF(Term Frequency): 이 토큰이 이 문서에 몇 번 나왔나.
  • IDF(Inverse Document Frequency): 이 토큰이 전체 문서 중 몇 개에만 나오나(희귀할수록 가중치↑).

강사의 정리: "BM25는 요거(토큰별 점수)의 가중치 합이다. 그게 BM25의 장점이다." 즉 질의의 각 토큰이 낸 점수를 시그마(합산) 하므로, 긴 질문에서도 부분 일치가 점수로 쌓인다.

밀집 벡터(Dense Vector) — 학습으로 얻은 벡터

밀집 벡터는 신경망의 역전파 학습(Backpropagation) 결과다. 각 차원이 서로 영향을 주며 하나의 의미를 표현하기 때문에, 사람이 공학적으로 만들 수 없다. 강사의 가장 중요한 통찰이 여기서 나온다.

"같은 손실(loss)로 학습된 임베딩끼리만 비교가 된다." Q1-3 임베딩으로 얻은 벡터와 다른 임베딩 모델로 얻은 벡터는 서로 비교되지 않는다. 손실이 똑같은 방향으로 학습됐기 때문에 그 모델끼리만 유사도 계산이 성립한다. (자막 04:43~04:50)

이것이 실무에서 "임베딩 모델을 섞어 쓰면 안 되는" 이론적 근거다. 인덱싱과 쿼리에 반드시 같은 모델을 써야 한다.

왜 둘을 섞는가

  • 희소(BM25)는 정확한 키워드·고유명사·코드 식별자에 강하지만, 동의어·문맥엔 약하다.
  • 밀집(Dense)은 의미·문맥·패러프레이즈에 강하지만, 드문 고유명사엔 약하다.

그래서 둘을 앙상블하면 서로의 약점을 메운다. 통합 방식이 다음 절의 RRF다.

💡 실무 노하우 — 코사인 유사도의 음수 처리: 강사는 임베딩 모델이 부정(Negative)을 학습하지 않으므로, 코사인 유사도 결과에서 마이너스 값은 버리라고 명시한다(01:40:40). 음수 코사인은 "반대 의미"가 아니라 그냥 신뢰할 수 없는 영역이다.


7. 변형 — Parent-Child · Multi-Query · HYDE · MRL · RRF

멀티벡터 스토어 패턴의 구체적 변형들과, 실무 인덱싱을 가능케 하는 기법들을 모은다.

7-1. Parent-Child Splitting (답변 확장)

작은 자식 청크로 검색하고, 매칭된 자식에 연결된 부모 원문을 컨텍스트로 가져온다. 검색은 정밀하게(작은 청크), 컨텍스트는 풍부하게(큰 청크) 가져가는 절충이다. LangChain ParentDocumentRetriever가 이 패턴을 캡슐화한다. 책은 부모를 인메모리에 넣었지만, 강사는 실무에선 반드시 DB 스토어로 빼라고 한다.

7-2. Multi-Query Retriever (질의 확장)

사용자의 단일 질문을 LLM으로 여러 관점의 질문으로 확장한 뒤, 각각 검색해 결과를 합친다. 별도 프롬프트 구현 없이 한 줄로 쓴다.

from langchain.retrievers.multi_query import MultiQueryRetriever

mq_retriever = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(), llm=llm)

⚠️ 주의: 내부적으로 LLM을 호출하므로 비용과 지연이 늘어난다. 질문이 짧거나 명확할 때는 과할 수 있다.

7-3. HYDE (Hypothetical Document Embedding)

질문 자체가 아니라, LLM이 만든 가상의 답변을 임베딩해서 실제 문서와 유사도를 잰다. 질문과 문서의 표현 격차를 줄이려는 옛 기법이다.

⚠️ 주의 — 지금은 필요성이 줄었다: 강사는 HYDE가 "구형 임베딩 시절의 기법"이라고 못 박는다. 최신 instruction-tuned 임베딩 모델(예: Q1-3 계열)은 질문과 답변의 임베딩 공간을 자동으로 맞춰 주기 때문에 직접 HYDE를 구현할 이유가 줄었다(01:33:36). 게다가 LLM 호출 비용이 추가되고, LLM이 답을 못 만드는 질문엔 통하지 않는다(01:30:34).

7-4. MRL 차원 축소 (Dense 인덱싱의 전제)

4000차원처럼 차원이 너무 높으면 HNSW 인덱스 구축에 문제가 생긴다. 그래서 차원을 줄이는데, 아무 모델이나 잘라도 되는 게 아니다.

MRL(Matryoshka Representation Learning) 학습된 모델만 앞에서부터 잘라도 의미가 유지된다. 강사: "이 임베딩 모델은 MRL 학습을 했단 말이야. MRL 학습을 하는 애들은 앞에서부터 자를 수가 있어. 요즘 최신 애들은 대부분 MRL 학습을 하고 나와요." (자막 04:46~04:48, 01:18:11)

MRL이 아니라면 PCA(주성분 분석) 같은 별도 기법으로 축소해야 한다(01:20:05). 예: 4000차원 → 1000차원.

7-5. BM25 직접 구현 (PostgreSQL)

pgvector 위에 BM25를 손으로 짓는다. 필요한 통계를 테이블로 관리한다.

  1. ts_vector: 한글 형태소 분석(MeCab-ko) 결과로 토큰별 등장 횟수를 벡터화한 필드.
  2. tf 테이블: 문서 ID별 토큰 등장 횟수.
  3. df 테이블: 각 토큰이 등장한 문서의 수.
  4. 문서 길이 테이블: 전체 문서 수와 총 토큰 길이 → 평균 문서 길이를 동적으로 관리.
  5. Gin 인덱스: ts_vector 필드에 걸어 검색 속도를 올린다.
  6. 트리거(Trigger): 문서 INSERT/UPDATE/DELETE 시 tf·df·평균 길이를 자동 갱신. 강사는 "인설트할 때마다 트리거를 두 군데 건다"고 설명한다(00:45:22, 00:47:01).

그 다음 BM25 공식을 CTE(Common Table Expression)로 직접 쿼리하고, 이를 LangChain BaseRetriever로 랩핑해 프레임워크 안에서 쓴다.

⚠️ 주의 — TS-RANK-CD의 함정: PostgreSQL 내장 ts_rank_cd로 긴 문장을 질의하면 AND 조건이 걸려 0건이 나오기 쉽다. BM25의 시그마(합산) 방식이 부분 일치를 점수로 쌓아 주므로 더 적합하다(00:56:03).

7-6. RRF (Reciprocal Rank Fusion) — 앙상블 통합

BM25와 Dense는 점수 체계가 완전히 다르다(한쪽은 BM25 스코어, 한쪽은 코사인 거리). 점수를 그대로 더할 수 없다. RRF는 점수 대신 "등수(Rank)" 를 쓴다. 각 리트리버에서 문서의 순위 r을 받아 1/(k + r) 형태로 합산해 최종 순위를 낸다.

from langchain.retrievers import EnsembleRetriever

ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, dense_retriever],
    weights=[0.5, 0.5],   # 두 신호의 비중
)

강사의 정리: "퓨전을 RRF라고 부르는데, 앙상블 리트리버의 기본값이 RRF로 되어 있다"(01:38:34~01:39:41). 서로 다른 스케일의 점수를 섞을 때 랭크 기반이 안전한 이유가 여기 있다.

7-7. 프롬프트의 오답 공간(Negative Space)

검색과 별개로, 최종 생성 프롬프트에서 강사가 강조한 원리다. LLM은 기본적으로 긍정 학습(positive learning) 을 해서 "무엇이 오답인지"를 스스로 정의하지 못한다. 강사의 예: "1 더하기 1은 2인데, 1 더하기 1의 오답은? 실수 전체다. 오답 공간이 너무 커서 갈 수가 없다." 그래서 프롬프트에 구성 조건·실패 조건·종료 조건오답 공간(무엇을 하지 말아야 하는지) 을 명시적으로 넣어야 모델이 의도대로 동작한다(01:27:18).


8. 다른 접근과의 비교

RAG vs Fine-tuning

  • Fine-tuning: 모델 가중치 자체에 지식을 새긴다. 도메인 문체·형식을 체화시키는 데 강하지만, 최신·private 데이터가 바뀔 때마다 재학습 비용이 든다.
  • RAG: 지식을 검색으로 붙인다. 문서를 갈아 끼우기만 하면 되어 최신성·유지보수에 유리하다.
  • 강의의 결론: 둘은 배타적이지 않다. 뒤에 소개된 Crew Studio(LoRA·Full Fine-tuning, PDF QA·OCR·Text-to-SQL 레시피)처럼, RAG로 검색을 붙이면서 필요한 부분만 미세 조정하는 병행이 실무적이다.

RAG vs Long Context

  • 컨텍스트 창이 커졌다고 문서를 통째로 밀어 넣으면, 비용·지연이 폭증하고 "건초더미 속 바늘" 문제로 정확도가 오히려 떨어진다. RAG는 필요한 조각만 골라 넣어 이 문제를 회피한다.

RAG vs Function Calling

  • Function Calling은 "행동(API 호출)"에, RAG는 "지식(문서 검색)"에 초점이 있다. 실무에선 함께 쓰인다 — 검색 결과를 근거로 함수를 호출하는 식.

왜 pgvector(PostgreSQL)인가 — vs MySQL, vs 전용 벡터 DB

강의의 저장소 선택 근거가 여기 있다.

  • vs MySQL: MySQL은 풀 텍스트 인덱싱 결과를 ts_vector처럼 외부 필드로 공개하지 않아 BM25를 직접 구현하기 어렵다. PostgreSQL은 ts_vector와 pgvector 확장을 함께 제공해 희소·밀집을 한 DB에서 다룬다(00:58:53).
  • vs 전용 벡터 DB: 이미 운영 중인 관계형 DB(트랜잭션·백업·권한)를 그대로 쓰면서 벡터 검색을 얹을 수 있어 운영 부담이 작다.

📚 참고: 강의는 크로마DB로 개념을 확인하되(HNSW·IVF·IVF-PQ 지원), 실무 저장소는 pgvector로 이전한다. "개념 확인용 도구"와 "운영 저장소"를 분리하는 접근 자체가 실무 팁이다.

보론 — 개발·운영 도구: pgAdmin과 Crew Studio

강의 후반부는 검색 엔진 자체가 아니라 그 주변 도구를 짚는다. 실무 RAG는 "코드"만으로 굴러가지 않기 때문이다.

  • pgAdmin: Docker로 띄운 웹 관리 도구로, pgvector의 테이블·데이터·인덱스를 눈으로 확인·관리한다(01:43:07). BM25 통계 테이블(tf·df·문서 길이)이 트리거로 제대로 갱신되는지, ts_vector가 형태소로 잘 쪼개졌는지 검증하는 데 특히 유용하다.
  • Crew Studio: LLM 미세 조정(Fine-tuning) 플랫폼으로, Fine-tuning 지식이 많지 않아도 클릭만으로 학습을 돌릴 수 있게 돕는다(01:47:27). 강의가 소개한 기능은 ① Hugging Face 모델을 GGUF 형식으로 내려받아 학습, ② LoRA(Low-Rank Adaptation) 또는 Full Fine-tuning 지원, ③ CSV·PDF·DOCX를 자동 파싱해 학습 데이터 생성, ④ PDF QA·OCR Extraction·Text-to-SQL 같은 레시피(Recipe) 제공이다(01:46:11~01:47:01).

이 두 도구의 위치는 명확하다. pgAdmin은 검색 인프라를 들여다보는 창이고, Crew Studio는 RAG로 부족할 때 모델 자체를 손보는 보완재다. §8 첫머리에서 말한 "RAG와 Fine-tuning은 배타적이지 않다"가 도구 차원에서 구현된 셈이다.

💡 실무 노하우: 검색이 원인인 오류(엉뚱한 문서를 물어 옴)와 생성이 원인인 오류(문서는 맞는데 답이 틀림)를 구분해야 한다. 전자는 검색 튜닝(청크·인덱스·앙상블 weight)으로, 후자는 프롬프트(오답 공간 정의) 또는 Fine-tuning으로 접근한다. 원인을 섞으면 엉뚱한 곳을 고치게 된다.


9. 한계

RAG와 이 강의 접근의 한계를 정직하게 짚는다.

  • 검색 품질 의존성: 최종 답변의 질은 검색 품질에 종속된다. 검색이 엉뚱한 조각을 물어 오면 LLM은 그 위에서 그럴듯한 오답을 만든다("garbage in, garbage out").
  • 청크 경계 오류(chunk boundary): 한 개념이 청크 경계에서 잘리면 어느 조각에도 온전히 담기지 않아 검색이 실패한다. chunk_overlap과 시맨틱 분할이 이를 완화하지만 완전히 없애지는 못한다.
  • 다국어·도메인 어휘: 한글은 형태소 분석기(MeCab-ko)가 없으면 BM25가 무너진다. 도메인 전문 용어·신조어는 형태소 사전에 없어 오분석될 수 있다.
  • 임베딩 모델 호환성: §6에서 봤듯 같은 손실로 학습된 모델끼리만 비교된다. 모델 교체 시 전체 재임베딩이 강제된다.
  • HYDE·Multi-Query의 비용: 검색 정확도를 위해 LLM을 추가 호출하면 비용·지연이 늘고, LLM이 답을 못 만드는 질문에는 오히려 독이 된다.
  • 인덱스 파라미터 튜닝 부담: HNSW의 M·ef_construction, BM25의 가중치, 앙상블 weight 등 손으로 맞출 것이 많다. 자동 최적화가 아니다.

  • 로컬 임베딩 서버의 불안정성: 강의는 LM Studio로 임베딩을 로컬 서빙하는데, 한 번에 보내는 텍스트가 많으면 연결이 끊긴다. 배치 크기를 작게 잡거나 재시도 로직을 넣지 않으면 대량 색인 중 조용히 실패한다.

  • GPU 리소스 소모: 임베딩 생성은 GPU를 많이 먹는다. 초기 대량 색인 시 병렬도(배치)를 잘못 잡으면 OOM이나 서버 다운으로 이어진다(00:12:23).

⚠️ 주의: 강사 스스로 실습 코드 곳곳에 [추정]·[확인 필요] 표기를 남겼다(예: per_layer_factor 값). 실습 값은 환경(GPU·모델·데이터)에 따라 달라지므로, 노트의 숫자를 정답이 아니라 출발점으로 받아들여야 한다.


10. 최신 패턴 (RAG 고도화 흐름)

강의가 다음 단계로 예고한 방향과, 업계에서 자리 잡은 RAG 고도화 패턴이다.

  • Agentic RAG: 검색을 한 번 하고 끝내지 않고, LLM 에이전트가 "검색 → 판단 → 재검색"을 반복하며 필요한 근거를 능동적으로 모은다. Multi-Query가 정적 확장이라면, Agentic RAG는 동적 확장이다.
  • GraphRAG: 문서를 벡터가 아니라 지식 그래프(엔티티·관계) 로 색인해, "여러 문서에 흩어진 사실을 연결해야 답이 되는" 질문에 강하다. 강의도 다음 챕터로 온톨로지 그래프·그래프 임베딩 연동을 예고한다(01:48:10).
  • Self-RAG: 모델이 스스로 "검색이 필요한가", "검색 결과가 근거로 충분한가"를 판단(reflection)하도록 학습·프롬프트하는 패턴.
  • CRAG(Corrective RAG): 검색 결과의 신뢰도를 평가해 낮으면 웹 검색 등으로 보정한 뒤 생성한다.
  • Re-ranker 결합: 1차로 넓게 검색(BM25+Dense)한 뒤, Cohere Rerank·BGE·Qwen3 Reranker 같은 재순위 모델로 상위 후보를 다시 정렬한다. 강의 화면에도 Qwen3 Reranker(0.6B/4B/8B)가 등장한다.

📚 참고: 이 강의의 하이브리드(BM25+Dense+RRF) 구조는 위 패턴들의 토대다. Agentic·Graph·Self/CRAG는 모두 "검색을 어떻게 더 똑똑하게 부를 것인가"의 상위 레이어이므로, 밑단 하이브리드 검색이 튼튼해야 위가 산다. (패턴명은 강의·업계 통용어 기준이며, 구체 도구·모델 버전은 각 공식 문서에서 확인할 것)


11. 메타인지 자기평가

배운 것을 "내가 실제로 할 수 있는가"로 바꾸는 체크리스트다.

검색 정확도를 측정할 수 있는가 - [ ] recall@K(정답 문서가 상위 K개 안에 들어오는 비율), MRR(정답이 나온 첫 순위의 역수 평균)로 검색기를 정량 평가할 수 있는가? - [ ] BM25 단독, Dense 단독, 앙상블(RRF) 세 가지를 같은 질의 셋으로 비교해 봤는가?

원리를 설명할 수 있는가 - [ ] 희소 벡터와 밀집 벡터의 생성 방식 차이(공학적 구성 vs 역전파 학습)를 남에게 설명할 수 있는가? - [ ] "같은 손실로 학습된 임베딩끼리만 비교된다"가 왜 모델 혼용을 막는지 설명할 수 있는가? - [ ] ts_rank_cd가 긴 질의에서 0건을 내는 이유와, BM25 시그마 방식이 왜 낫는지 말할 수 있는가?

실무로 옮길 수 있는가 - [ ] MeCab-ko가 없을 때 한글 BM25가 왜 무너지는지 재현해 봤는가? - [ ] 4000차원 임베딩에 HNSW를 걸다 막혔을 때, MRL 축소와 PCA 중 무엇을 언제 쓰는지 판단할 수 있는가? - [ ] EnsembleRetriever의 weight를 바꿔 가며 recall이 어떻게 움직이는지 관찰하고, Re-ranker를 붙인 뒤 재측정할 계획을 세울 수 있는가?

다음 단계 - [ ] 파이썬 프로토타입을 Rust/TypeScript로 옮기거나 특정 기능만 API로 빼서 도커에 올리는 전환 경로를 그릴 수 있는가? - [ ] GraphRAG(온톨로지 그래프)로 확장할 때, 지금의 하이브리드 검색을 어떻게 재사용할지 구상할 수 있는가?

확인: 위 항목 중 절반 이상에 "예"라고 답할 수 없다면, 해당 절(§6 원리 / §7 변형 / §4 구현)로 돌아가 실습을 한 번 더 돌리는 것이 좋다. RAG는 "읽어서 아는 것"과 "돌려서 아는 것"의 간극이 특히 큰 주제다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플   P 음성 버전 2026-08-05b